package com.dferreira.repository.services;

import android.content.Context;
import android.text.TextUtils;
import android.util.Log;

import com.dferreira.languagesteach.utils.DataPaths;
import com.dferreira.repository.RepositoryDatabaseHelper;
import com.dferreira.repository.data_models.Dependency;
import com.dferreira.repository.data_models.FileEntry;
import com.dferreira.repository.data_models.PackageCommon;
import com.dferreira.repository.data_models.PackageMetadata;
import com.dferreira.repository.data_models.PackageMetadataState;
import com.dferreira.repository.data_models.Repository;
import com.dferreira.repository.data_models.RepositoryPackage;
import com.dferreira.repository.data_utils.FileEntryZIPExtractor;
import com.j256.ormlite.dao.RuntimeExceptionDao;
import com.j256.ormlite.stmt.DeleteBuilder;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.stmt.Where;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

public class PackageInstaller {

    private final static String TAG = "InstallExternalPackageTask";
    private final Context context;
    private FileEntryZIPExtractor zipPackageUtils;
    private RepositoryPackage pkgInfo;
    private final DataPaths dataPaths;
    private PackageInstallerState installState;
    private int numCurrentPkg;
    private int numCurrentFile;
    private boolean finishedWell;
    private RepositoryDatabaseHelper repositoryDbHelper;
    private boolean cancelled;
    private RepositoryNotifier repositoryNotifier;

    /**
     * Constructor
     *
     * @param context Information about an application environment.
     * @param dataPaths
     * @param repositoryDbHelper
     * @param repositoryNotifier
     */
    public PackageInstaller(Context context, DataPaths dataPaths,RepositoryDatabaseHelper repositoryDbHelper, RepositoryNotifier repositoryNotifier) {
        this.context = context;
        this.dataPaths = dataPaths;
        this.repositoryDbHelper = repositoryDbHelper;
        this.repositoryNotifier = repositoryNotifier;
    }

    /**
     * @return
     */
    private boolean isCancelled() {
        return cancelled;
    }

    /**
     * Creates a list from the packages that are required to install to
     * get that package installed
     *
     * @param repositoryPkg packaged to install
     * @return list from the packages to install
     */
    private ArrayList<RepositoryPackage> createListFromPackagesToInstall(
            RepositoryPackage repositoryPkg) {
        ArrayList<RepositoryPackage> createdLst;
        ArrayList<RepositoryPackage> packagesToExpand;
        RepositoryPackage expand;
        Iterator<RepositoryPackage> iDependencies;

        createdLst = new ArrayList<RepositoryPackage>();
        packagesToExpand = new ArrayList<RepositoryPackage>();

        packagesToExpand.add(repositoryPkg);

        while (packagesToExpand.size() != 0) {
            expand = packagesToExpand.remove(0);
            createdLst.add(expand);
            if (expand.getDependencies() != null) {
                iDependencies = expand.getDependencies().iterator();
                while (iDependencies.hasNext()) {
                    RepositoryPackage repDependency = iDependencies.next();
                    packagesToExpand.add(repDependency);
                }
            }
        }
        Collections.reverse(createdLst);
        return createdLst;
    }

    /**
     * @param packageMetadata information from the package to set on the database
     * @return false   -> everything alright
     * true    -> an error happened
     */
    private boolean putDependenciesOnTable(PackageMetadata packageMetadata) {
        Iterator<PackageMetadata> dependencies;
        PackageMetadata pkgDependency;

        try {
            deleteDependenciesByPackage(packageMetadata);

            dependencies = packageMetadata.getDependencies().iterator();
            while (dependencies.hasNext()) {
                pkgDependency = dependencies.next();
                Dependency dependency = new Dependency();
                dependency.setForPackage(packageMetadata);
                dependency.setFromPackage(pkgDependency);
                createDependency(dependency);
            }
        } catch (android.database.SQLException e) {
            Log.w(TAG, e);
            return true;
        }
        return false;
    }

    /**
     * creates a repositoryPackage instance from a packageMetadata
     *
     * @param rPkg reference to the repositoryPackage where the package will be installed
     * @return packageMetadata reference
     */
    private PackageMetadata RepositoryPackage2PackageMetadata(
            RepositoryPackage rPkg) {
        PackageMetadata pkg;
        pkg = new PackageMetadata();


        pkg.setRepository(rPkg.getRepository());
        pkg.setName(rPkg.getName());
        pkg.setDescription(rPkg.getDescription());
        pkg.setVersion(rPkg.getVersion());
        pkg.setAuthor(rPkg.getAuthor());
        pkg.setCompressedSize(rPkg.getCompressedSize());
        pkg.setUncompressedSize(rPkg.getUncompressedSize());
        pkg.setUrl(rPkg.getUrl());
        pkg.setPath(dataPaths.getPackagesPath());

        return pkg;
    }

    /**
     * Will save a new package in the table where is keep the info from the
     * package
     *
     * @param rPkg a reference to a RepositoryPackage
     * @return A reference to the repositoryPackage that was persisted in database
     */
    private PackageMetadata savePkgOnTable(RepositoryPackage rPkg, PackageMetadataState state) {
        PackageMetadata pkg;

        pkg = RepositoryPackage2PackageMetadata(rPkg);
        pkg.setState(state);

        savePackageMetadata(pkg);


        return pkg;
    }

    //Goal avoiding nulls
    private void copyRepositoryPackageToPackageMetadata(RepositoryPackage repositoryPkg, PackageMetadata packageMetadata) {
        packageMetadata.setName(repositoryPkg.getName());
        packageMetadata.setDescription(repositoryPkg.getDescription());
        packageMetadata.setVersion(repositoryPkg.getVersion());
        packageMetadata.setAuthor(repositoryPkg.getAuthor());
        packageMetadata.setCompressedSize(repositoryPkg.getCompressedSize());
        packageMetadata.setUncompressedSize(repositoryPkg.getUncompressedSize());

    }


    /**
     * @param repositoryPkg
     * @return
     */
    private PackageMetadata getPackageMetadataByRepositoryPackage(RepositoryPackage repositoryPkg) {
        PackageMetadata packageMetadata;
        try {
            RuntimeExceptionDao<PackageMetadata, Integer> dao = repositoryDbHelper.getPackageMetadataDao();
            QueryBuilder<PackageMetadata, Integer> builder = dao.queryBuilder();
            Where<PackageMetadata, Integer> where = builder.where();
            where.eq(PackageMetadata.COLUMN_NAME_NAME, repositoryPkg.getName());
            builder.limit(1l);
            packageMetadata = dao.queryForFirst(builder.prepare());
            if (packageMetadata == null) {
                packageMetadata = new PackageMetadata();
                copyRepositoryPackageToPackageMetadata(repositoryPkg, packageMetadata);
                packageMetadata.setState(PackageMetadataState.New);
            }
            return packageMetadata;
        } catch (java.sql.SQLException e) {
            Log.w(TAG, e);
            return null;
        }
    }


    /**
     * @param repositoryPkg A reference to the RepositoryPackage
     * @param stateName     state to verify if the package already there
     * @return null Will need to install a new version not null Last version
     * already installed
     */
    private PackageMetadata lastVersionAlreadyExists(RepositoryPackage repositoryPkg, String stateName) {
        PackageMetadata packageMetadata;

        packageMetadata = getPackageMetadataByRepositoryPackage(repositoryPkg);

        if ((packageMetadata.getState().equals(stateName))
                && (packageMetadata.getVersion().equals(repositoryPkg.getVersion()))) {
            return packageMetadata;
        } else {
            return null;
        }
    }

    /**
     * Iterates over an repositoryPkg and build an array from dependencies
     *
     * @param repositoryPkg A reference to the RepositoryPackage
     * @return a list of dependencies that will be require to install
     */
    private List<PackageMetadata> generateDependenciesArray(
            RepositoryPackage repositoryPkg) {
        Iterator<RepositoryPackage> iDependencies;
        ArrayList<PackageMetadata> pkgDependencies;
        RepositoryPackage repDependency;
        PackageMetadata dependency;

        if (repositoryPkg.getDependencies() != null) {
            iDependencies = repositoryPkg.getDependencies().iterator();
            pkgDependencies = new ArrayList<PackageMetadata>(repositoryPkg
                    .getDependencies().size());
            while (iDependencies.hasNext()) {
                repDependency = iDependencies.next();
                dependency = RepositoryPackage2PackageMetadata(repDependency);
                pkgDependencies.add(dependency);
            }
            return pkgDependencies;
        } else {
            return null;
        }
    }

    /**
     * Unzip the files from a package
     *
     * @param repositoryPkg A reference to the RepositoryPackage
     */
    private int decompressesPkgFiles(PackageMetadata repositoryPkg) {
        URL pkgUrl;
        URLConnection urlConnection;
        long entrySize;
        /* Data variables */
        InputStream inputStream;


        try {
            pkgUrl = new URL(repositoryPkg.getUrl());
            urlConnection = pkgUrl.openConnection();
            urlConnection.setConnectTimeout(dataPaths.getConnectTimeout());
            urlConnection.setReadTimeout(dataPaths.getConnectTimeout());
            inputStream = urlConnection.getInputStream();
            zipPackageUtils.beginInstallingPackage(inputStream);
            this.numCurrentFile = 0;
            while (zipPackageUtils.fetchNextEntry()) {
                if (numCurrentFile > installState.getNumCurrentFile()) {
                    entrySize = zipPackageUtils.installAnEntry(repositoryPkg);
                    if (entrySize < 0) {
                        break;
                    }
                    installState.incFileNum(entrySize);
                    if (isCancelled() || !(installState.isInstalling())) {
                        return installState.getNumCurrentFile();
                    }
                }
                numCurrentFile++;
                repositoryNotifier.broadcastIntentWithState(10);
            }
        } catch (IOException e) {
            Log.w(TAG, e);
            return 0;
        }
        return installState.getNumCurrentFile();
    }

    /**
     * @param repositoryPkg A reference to the RepositoryPackage
     * @return metadata from the package
     */
    private PackageMetadata installPackage(RepositoryPackage repositoryPkg) {
        /* SQLite reference */
        Iterator<RepositoryPackage> iPackages;
        ArrayList<RepositoryPackage> packagesToInstall;
        /* Data variables */
        PackageMetadata pkgMetadata;
        List<PackageMetadata> pkgDependencies;

        packagesToInstall = createListFromPackagesToInstall(repositoryPkg);
        iPackages = packagesToInstall.iterator();

        numCurrentPkg = 0;
        pkgMetadata = null;
        while (iPackages.hasNext() && !isCancelled() && installState.isInstalling()) {
            RepositoryPackage currentPkg = iPackages.next();

            if (numCurrentPkg > installState.getNumCurrentPkg()) {
                pkgMetadata = lastVersionAlreadyExists(currentPkg, PackageMetadataState.Installed.name());
                if (pkgMetadata != null) {
                    numCurrentPkg++;
                    installState.skipPkgNum();
                    continue;
                }
                // Install the package structure and associates with his
                // dependencies
                pkgMetadata = savePkgOnTable(currentPkg, PackageMetadataState.Created);
                pkgDependencies = generateDependenciesArray(repositoryPkg);
                if (pkgDependencies != null) {
                    updatePkgStateAttribute(currentPkg.getName(), PackageMetadataState.InstallingDependencies)
                    ;
                    pkgMetadata.setDependencies(pkgDependencies);
                    if (putDependenciesOnTable(pkgMetadata)) {
                        return null;
                    }
                }
                installState.addPkg(pkgMetadata.getId());
                updatePkgStateAttribute(currentPkg.getName(), PackageMetadataState.InstallingFiles);
            } else {
                pkgMetadata = lastVersionAlreadyExists(currentPkg, PackageMetadataState.InstallingFiles.name());
            }
            // Install package files
            if (numCurrentPkg == installState.getNumCurrentPkg()) {
                if (decompressesPkgFiles(pkgMetadata) == 0) {
                    //Could not install the package
                    updatePkgStateAttribute(currentPkg.getName(), PackageMetadataState.Damaged);
                    return null;
                }
            }
            numCurrentPkg++;
        }
        // finally validates the saved packages
        iPackages = packagesToInstall.iterator();
        if (numCurrentPkg == packagesToInstall.size()) {
            while (iPackages.hasNext() && !isCancelled()
                    && installState.isInstalling()) {
                RepositoryPackage currentPkg = iPackages.next();
                updatePkgStateAttribute(currentPkg.getName(), PackageMetadataState.Installed);
            }
        }
        return pkgMetadata;
    }

    /**
     * Will purge the files from a package and delete his entries from database
     *
     * @param packageFileEntries
     */

    private void removeFilesAndMetadataAFromPackage(List<FileEntry> packageFileEntries) {
        File file;
        File[] children;

        if ((packageFileEntries == null) || packageFileEntries.isEmpty()) {
            return;
        }
        for (int i = packageFileEntries.size(); i != 0; i--) {
            FileEntry fileEntry = packageFileEntries.get(i - 1);
            if (!TextUtils.isEmpty(fileEntry.getPath())) {
                file = new File(fileEntry.getPath());
                if (file.exists()) {
                    if (file.isDirectory()) {
                        children = file.listFiles();
                        if (children != null) {
                            for (File aChildren : children) {
                                if (!aChildren.delete()) {
                                    break;
                                }
                            }
                        }
                    }
                    if (file.delete()) {
                        deleteFileEntry(fileEntry);
                    }

                }
            }
            installState.decFileNum(fileEntry.getSize());
        }
    }

    /**
     * Will revert the installation process and clean files down
     *
     * @return false   -> something wrong
     * true    -> everything was just fine
     */
    private boolean revertPackageInstallation() {
        Integer lastPkgId;
        boolean reverted;
        List<FileEntry> packageFileEntries;

        reverted = false;
        while (installState.getLastPkgID() != null) {
            lastPkgId = installState.getLastPkgID();
            packageFileEntries = getFileEntriesByPackageId(lastPkgId);
            removeFilesAndMetadataAFromPackage(packageFileEntries);
            // Purge the package from database
            deletePackageMetadata(lastPkgId);

            installState.subPkg();
            repositoryNotifier.broadcastIntentWithState(5);
        }
        reverted = true;
        return reverted;
    }


    /**
     * Called externally in order to install some package
     *
     * @param pkgInfo      A reference to the RepositoryPackage
     * @param installState State from the package to install
     */
    public void installPackage(RepositoryPackage pkgInfo,
                               PackageInstallerState installState) {
        this.pkgInfo = pkgInfo;
        this.installState = installState;

        this.zipPackageUtils = new FileEntryZIPExtractor(context);
        this.zipPackageUtils.setPackageFolder(dataPaths.getPackagesPath());

        this.numCurrentPkg = 0;
        this.numCurrentFile = 0;
        this.finishedWell = false;
        this.execute();
    }

    /**
     *
     */
    private Long execute() {
        StringBuilder strLang;
        Locale locale;
        PackageMetadata installResult;
        boolean uninstallResult;

        uninstallResult = false;
        locale = new Locale(pkgInfo.getName());
        strLang = new StringBuilder(locale.getDisplayLanguage());
        strLang.setCharAt(0, Character.toUpperCase(strLang.charAt(0)));
        installResult = installPackage(pkgInfo);
        if ((installState.isRunning() && !installState.isInstalling()) || ((installResult == null) && !isCancelled())) {
            uninstallResult = revertPackageInstallation();
        }
        if ((installState.isInstalling() && (installResult != null))
                || (!installState.isInstalling())
                && (uninstallResult))
            finishedWell = true;
        installState.setNotRunning();
        return (long) 100;
    }


    /**
     * @return the percentage of installation progress
     */
    public Integer getInstallationProgress() {
        int installedPercentage;

        installedPercentage = (int) ((installState.getTotalBytesInstalled() * 100) / (installState
                .getTotalBytesToInstall()));
        if (installedPercentage > 100) {
            installedPercentage = 100;
        }
        if (!installState.isRunning()) {
            if (!finishedWell) {
                return null;
            } else {
                if (installState.isInstalling()) {
                    return 100;
                } else {
                    return 0;
                }
            }
        }

        return installedPercentage;
    }

    /**
     * Sets the state to revert the installation
     */
    public void revertInstallation() {
        installState.setAsNotInstalling();
    }


    /**
     * @param packageMetadata
     * @return
     */
    private int savePackageMetadata(PackageMetadata packageMetadata) {
        RuntimeExceptionDao<PackageMetadata, Integer> dao = repositoryDbHelper.getPackageMetadataDao();
        if (packageMetadata.getId() == null) {
            return dao.create(packageMetadata);
        } else {
            return dao.update(packageMetadata);
        }

    }


    /**
     * @param name
     * @param state
     * @return
     */
    private int updatePkgStateAttribute(String name, PackageMetadataState state) {
        try {
            RuntimeExceptionDao<PackageMetadata, Integer> dao = repositoryDbHelper.getPackageMetadataDao();
            QueryBuilder<PackageMetadata, Integer> builder = dao.queryBuilder();
            Where<PackageMetadata, Integer> where = builder.where();
            where.eq(PackageCommon.COLUMN_NAME_NAME, name);
            builder.limit(1l);
            PackageMetadata packageMetadata = dao.queryForFirst(builder.prepare());
            packageMetadata.setState(state);
            return dao.update(packageMetadata);
        } catch (java.sql.SQLException e) {
            Log.w(TAG, e);
            return 0;
        }
    }

    /**
     * method for insert a dependency
     *
     * @param dependency
     * @return
     */
    private int createDependency(Dependency dependency) {
        RuntimeExceptionDao<Dependency, Integer> dao = repositoryDbHelper.getDependencyDao();
        int i = dao.create(dependency);
        return i;
    }

    /**
     * method for delete dependencies from package
     *
     * @param packageMetadata Package that has the dependencies
     */
    private void deleteDependenciesByPackage(PackageMetadata packageMetadata) {
        try {
            RuntimeExceptionDao<Dependency, Integer> dao = repositoryDbHelper.getDependencyDao();

            QueryBuilder<Dependency, Integer> builder = dao.queryBuilder();
            Where<Dependency, Integer> where = builder.where();
            where.eq(Dependency.COLUMN_NAME_FOR_PACKAGE, packageMetadata);
            List<Dependency> list = dao.query(builder.prepare());
            dao.delete(list);
        } catch (java.sql.SQLException e) {
            Log.w(TAG, e);
        }
    }


    /**
     * @param packageId the id from the package to delete
     */
    private void deletePackageMetadata(Integer packageId) {
        try {
            RuntimeExceptionDao<PackageMetadata, Integer> dao = repositoryDbHelper.getPackageMetadataDao();

            QueryBuilder<PackageMetadata, Integer> builder = dao.queryBuilder();
            Where<PackageMetadata, Integer> where = builder.where();
            where.eq(PackageCommon.COLUMN_NAME_ID, packageId);
            List<PackageMetadata> list = dao.query(builder.prepare());
            dao.delete(list);
        } catch (java.sql.SQLException e) {
            Log.w(TAG, e);
        }
    }

    /**
     * @param packageId
     * @return
     */
    private List<FileEntry> getFileEntriesByPackageId(Integer packageId) {
        try {
            RuntimeExceptionDao<FileEntry, Integer> dao = repositoryDbHelper.getFileEntryDao();
            QueryBuilder<FileEntry, Integer> builder = dao.queryBuilder();
            Where<FileEntry, Integer> where = builder.where();
            where.eq(FileEntry.COLUMN_NAME_PACKAGE_METADATA_ID, packageId);
            return dao.query(builder.prepare());
        } catch (java.sql.SQLException e) {
            Log.w(TAG, e);
            return null;
        }
    }

    /**
     * @param fileEntry
     */
    private void deleteFileEntry(FileEntry fileEntry) {
        try {
            RuntimeExceptionDao<FileEntry, Integer> dao = repositoryDbHelper.getFileEntryDao();
            DeleteBuilder<FileEntry, Integer> builder = dao.deleteBuilder();
            Where<FileEntry, Integer> where = builder.where();
            where.eq(FileEntry.COLUMN_NAME_ID, fileEntry.getId());
            builder.delete();
        } catch (java.sql.SQLException e) {
            Log.w(TAG, e);
        }
    }
}